﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace ExpressionEvaluator
{
    /// <summary>
    /// Expression Eval Exceptions
    /// </summary>
    public class ExprEvaluatorException : Exception
    {

        /// <summary>
        /// ExprEvaluatorException
        /// </summary>
        /// <param name="argEx">wrapped up so all expr errors cna be handled</param>
        public ExprEvaluatorException(ArgumentOutOfRangeException argEx)
            : base(argEx.Message, argEx)
        {
        }

        /// <summary>
        /// ExprEvaluatorException
        /// </summary>
        /// <param name="expression"></param>
        /// <param name="message"></param>
        public ExprEvaluatorException(string expression, string message)
            : base(string.Format("Invalid equation segment: {0} ; {1}", expression, message))
        {
        }
    }
    /// <summary>
    /// CustomEquation
    /// </summary>
    public class ExprEvaluator
    {
        #region members
        private List<char> _validOperators = new List<char>() { '+', '-', '*', '/', '^' };
        private string _expression = "";
        private bool _istestMode = false;
        private decimal _conversonFactor = 1;


        private void ThrowEquationException(string message = "")
        {
            throw new ExprEvaluatorException(_expression, message);
        }
        #endregion

        #region Properties
        /// <summary>
        /// input object to use in variable calculations
        /// </summary>
        public dynamic InputObj
        {
            get;
            set;
        }

        ///// <summary>
        ///// disable in line functions from beign found
        ///// </summary>
        //public bool DisableInLineFunctions
        //{
        //    get;
        //    set;
        //}


        private ExprCalculationInfo _calcInfo = null;
        private ExprCalculationInfo CalcInfo
        {
            get
            {
                if (_calcInfo == null)
                    _calcInfo = new ExprCalculationInfo();
                return _calcInfo;
            }
        }
      
        /// <summary>
        /// variables
        /// </summary>
        public Dictionary<string, Func<dynamic, decimal>> Variables
        {
            get
            {
                return CalcInfo.Variables;
            }
        }

        /// <summary>
        /// constants
        /// </summary>
        public Dictionary<string, decimal> Constants
        {
            get
            {
                return CalcInfo.Constants;
            }
        }

        /// <summary>
        /// add custom inline function
        /// </summary>
        /// <param name="key"></param>
        /// <param name="description"></param>
        /// <param name="argNumber"></param>
        /// <param name="inLineFunc"></param>
        public void AddInlineFunction(string key, string description, int argNumber, Func<decimal[], decimal> inLineFunc)
        {
            CalcInfo.InLine_Functions.Add(key, new ExprCalculationInfo.InLineFuncContainer(inLineFunc, argNumber, description));
        }

        /// <summary>
        /// add custom inline function
        /// </summary>
        /// <param name="key"></param>
        /// <param name="argNumber"></param>
        /// <param name="inLineFunc"></param>
        public void AddInlineFunction(string key, int argNumber, Func<decimal[], decimal> inLineFunc)
        {
            CalcInfo.InLine_Functions.Add(key, new ExprCalculationInfo.InLineFuncContainer(inLineFunc, argNumber, ""));
        }

        /// <summary>
        /// Inline functions descriptions that can be used in the equation
        /// </summary>
        public Dictionary<string, string> InLine_FunctionDescriptions
        {
            get
            {
                return CalcInfo.InLine_FunctionDescriptions;
            }
        }

        private int _Precision = 5;
        /// <summary>
        /// Precision
        /// </summary>
        public int Precision
        {
            get { return _Precision; }
            set
            {
                try
                {
                    Math.Round(0M, value);//test if we can round
                }
                catch (ArgumentOutOfRangeException argEx)//not valid for Precision
                {
                    throw new ExprEvaluatorException(argEx);
                }
                _Precision = value;
            }
        }

      
        /// <summary>
        /// current conversion factor to apply at end
        /// </summary>
        public decimal ConversionFactor
        {
            get
            {
                return _conversonFactor;
            }
            set
            {
                _conversonFactor = value;
            }
        }
        /// <summary>
        /// Equation
        /// </summary>
        public string Expression
        {
            get { return _expression; }
            set
            {
                _expression = value;

                ValidateEquation();

                //spread out the equation to make parsing easier
                _validOperators.ForEach(op => _expression = _expression.Replace(op.ToString(), string.Format(" {0} ", op.ToString())));
                _expression = _expression.Replace(",", " , ");
                _expression = _expression.Replace("(", "( ");
                _expression = _expression.Replace("{", "{ ");
            }
        }

        #endregion
        /// <summary>
        /// def constr
        /// </summary>
        public ExprEvaluator()
        {
            _calcInfo = new ExprCalculationInfo();
        }

        /// <summary>
        /// constr with setting of equation
        /// </summary>
        /// <param name="equationIn"></param>
        public ExprEvaluator(string equationIn)
        {
            this.Expression = equationIn;
            _calcInfo = new ExprCalculationInfo();
        }

        /// <summary>
        /// private constr
        /// </summary>
        /// <param name="equationIn"></param>
        /// <param name="CalcInfo"></param>
        private ExprEvaluator(string equationIn, ExprCalculationInfo CalcInfo)
        {
            this.Expression = equationIn;
            _calcInfo = CalcInfo;
        }


        /// <summary>
        /// private constr
        /// </summary>
        /// <param name="equationIn"></param>
        /// <param name="CalcInfo"></param>
        private ExprEvaluator(string equationIn, ExprCalculationInfo CalcInfo, dynamic inputObj)
        {
            this.InputObj = inputObj;
            this.Expression = equationIn;
            _calcInfo = CalcInfo;
        }

        /// <summary>
        /// validate equation
        /// </summary>
        private void ValidateEquation()
        {
            if (_expression == null)
                ThrowEquationException();
            _expression = _expression.Trim();
            if (_expression.Length == 0)
            {
                ThrowEquationException();
            }
        }

        #region Public

        /// <summary>
        /// validate if expression can be evaluated
        /// </summary>
        /// <returns></returns>
        public bool Validate()
        {
            try
            {
                PerformCalculation(true);
                return true;
            }
            catch
            {
                return false;
            }
        }

        /// <summary>
        /// PerformCalculation
        /// </summary>
        /// <param name="istest">executes as test</param>
        /// <returns></returns>
        public decimal PerformCalculation(bool istest = false)
        {

            return Math.Round(calculate(istest) * _conversonFactor, Precision);
        }

        #endregion

        #region calculate
        private decimal calculate(bool istest = false)
        {
            ValidateEquation();

            List<char> eqOperators = new List<char>();
            List<decimal> eqValues = new List<decimal>();


            _istestMode = istest;

            ExprEvaluator eq;

            string innerEq;
            int indOfParen;

            string strFunc = "";

            decimal innerRetVal;

            char curChar = '(';//cuurent char
            char parenStart = '(';//current paren surrounding start
            char parenEnd = ')';//curretn paren surrounding end
            while ((indOfParen = _expression.IndexOfAny(new char[] { ')', '}' })) != -1)
            {
                parenEnd =_expression[indOfParen];
                if (parenEnd == ')')
                    parenStart = '(';
                else
                    parenStart = '{';

                for (int i = indOfParen; i > -1; i--)
                {
                    curChar = _expression[i];

                    if (curChar == parenStart)//found mathcing paren
                    {
                        innerRetVal = 1;
                        strFunc = "";
                        int j = 0;
                        if (i > 0)
                        {
                            for (j = i - 1; j > 0; j--)
                            {
                                if (_expression[j] == ' ' || _expression[j] == parenStart)
                                    break;
                            }
                            strFunc = _expression.Substring(j, i - j).Trim();
                        }



                        innerEq = _expression.Substring(i + 1, indOfParen - 1 - i);

                        if (strFunc.Length > 0)
                        {
                            if (!this._calcInfo.InLine_Functions.ContainsKey(strFunc.ToUpper()))
                                ThrowEquationException(string.Format("Invalid InLine Function name: {0} ", strFunc));

                            string[] inLineFuncStrArgs = SplitArgs(innerEq);
                            decimal[] inLineFuncDecArgs = new decimal[inLineFuncStrArgs.Length];
                            for (int k = 0; k < inLineFuncStrArgs.Length; k++)
                            {
                                eq = new ExprEvaluator(inLineFuncStrArgs[k], _calcInfo, InputObj);
                                inLineFuncDecArgs[k] = eq.calculate(istest);
                            }
                            try
                            {
                                innerRetVal = _calcInfo.ExecuteInlineFunc(strFunc.ToUpper(), inLineFuncDecArgs);
                            }
                            catch (Exception ex)
                            {
                                ThrowEquationException(string.Format("Error Calling InLine Function: {0} ;\nError: {1}", strFunc.ToUpper(), ex.Message));
                            }

                        }
                        else
                        {
                            eq = new ExprEvaluator(innerEq, _calcInfo, InputObj);
                            innerRetVal = eq.calculate(istest);
                        }
                        _expression = _expression.Replace(strFunc + parenStart + innerEq + parenEnd, string.Format(" {0} ", innerRetVal.ToString()));

                        break;
                    }
                    if (i == 0)
                        ThrowEquationException("Parenthesis Mismatch");
                }
            }
            if (_expression.Contains('(') || _expression.Contains('{'))
                ThrowEquationException("Parenthesis Mismatch");



            string[] splitEq = null;

            splitEq = _expression.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);

            int lastValCnt = 0;//if two or more operands are in a row we need to modify the coming value to have the right sign
            List<char> opTrail = new List<char>();//used to keep track operands.



            for (int i = 0; i < splitEq.Length; i++)
            {

                splitEq[i] = splitEq[i].Trim().ToUpper();

                if (splitEq[i].Length == 1)
                {
                    char op = splitEq[i][0];
                    if (_validOperators.Contains(op))
                    {
                        if (eqValues.Count == 0 && op != '+' && op != '-')
                        {
                            if (opTrail.Count > 0)
                                ThrowEquationException("Check that you have the proper Operators, Operands and Variables");
                            else
                                ThrowEquationException(string.Format("Cannot start a segment with this type of operator: '{0}'", op));
                        }
                        else
                        {
                            if (lastValCnt == eqValues.Count)
                            {//try to merge operands i.e. + -4
                                if (op != '+' && op != '-')
                                    ThrowEquationException("Check that you have the proper Operators, Operands and Variables");
                                opTrail.Add(op);

                            }
                            else
                            {
                                if (op == '+' || op == '-')
                                {
                                    opTrail = new List<char>() { op };
                                    if (op == '-')
                                        op = '+';//always use '+' in the operand list for '+' or '-' cause we can let the GetSign method get the correct sign
                                }
                                eqOperators.Add(op);
                            }
                            lastValCnt = eqValues.Count;
                        }
                        continue;
                    }
                }

                int signOfNextVal = GetSignFromOps(opTrail);
                opTrail.Clear();
                decimal ValToAdd = 0;
                if (_calcInfo.Constants.ContainsKey(splitEq[i]))
                {
                    ValToAdd = signOfNextVal * _calcInfo.Constants[splitEq[i]];
                }
                else
                {
                    ValToAdd = signOfNextVal * ProcessVar(splitEq[i]);
                }
                eqValues.Add(ValToAdd);

            }


            if (eqValues.Count != eqOperators.Count + 1)
                ThrowEquationException("Check that you have the proper Operands and Variables");


            //actually calculate the equation
            decimal val = eqValues[0];


            List<List<char>> opsToHandle = new List<List<char>>() { new List<char>() { '^' }, new List<char>() { '*', '/' }, new List<char>() { '+', '-' } };

            foreach (List<char> ops in opsToHandle)
            {
                for (int i = 0; i < eqOperators.Count; i++)
                {
                    if (ops.Contains(eqOperators[i]))
                    {
                        val = Calc(eqValues[i], eqOperators[i], eqValues[i + 1]);
                        eqValues.RemoveAt(i);
                        eqValues[i] = val;
                        eqOperators.RemoveAt(i);
                        i--;
                    }
                }
            }
            return val;
        }


        /// <summary>
        /// splits arguments 
        /// </summary>
        /// <param name="innerEq"></param>
        /// <returns></returns>
        private string[] SplitArgs(string innerEq)
        {
            List<string> retArgs= new List<string>();
            int intParenCount = 0;
            int intCurlyParenCount = 0;
            StringBuilder curArgument = new StringBuilder();
            foreach (char ch in innerEq)
            {
                switch(ch)
                {
                    case '(':
                        intParenCount++;
                        break;
                    case ')':
                        intParenCount--;
                        break;
                    case '{':
                        intCurlyParenCount++;
                        break;
                    case '}':
                        intCurlyParenCount--;
                        break;
                    case ',':
                        if(intParenCount == 0 && intCurlyParenCount == 0)
                        {
                            retArgs.Add(curArgument.ToString());//append cur argument
                            curArgument.Clear();
                        }
                        break;
                    default:
                        curArgument.Append(ch);//append argument content
                        break;
                }                 
            }
            if(curArgument.Length> 0&& intParenCount == 0 && intCurlyParenCount == 0)
                retArgs.Add(curArgument.ToString());//append cur argument

            return retArgs.ToArray();
        }

        private int GetSignFromOps(List<char> opTrail)
        {
            //'--- = -' -+- =  ERROR' and so on


            bool hasSignSwitched = false; // sign can only switch once orelse the operan string is invalid

            if (opTrail.Count == 0)
                return 1;

            int sign = opTrail[0] == '+' ? 1 : -1;//default to first
            char prevVal = opTrail[0];

            for (int i = 1; i < opTrail.Count; i++)
            {
                if (prevVal != opTrail[i])
                {
                    if (!hasSignSwitched)
                        hasSignSwitched = true;
                    else
                        ThrowEquationException("Check that you have the proper Operators, Operands and Variables");
                }
                sign = sign * (opTrail[i] == '+' ? 1 : -1);
                prevVal = opTrail[i];
            }
            return sign;
        }

        private decimal Calc(decimal operand1, char op, decimal operand2)
        {
            switch (op)
            {
                case '+':
                    return operand1 + operand2;
                case '-':
                    return operand1 - operand2;
                case '*':
                    return operand1 * operand2;
                case '/':
                    return operand1 / operand2;
                case '^':
                    return (decimal)Math.Pow((double)operand1, (double)operand2);
                default:
                    ThrowEquationException(string.Format("Invalid Operand: '{0}'", op));
                    return 0;
            }
        }
        private decimal ProcessVar(string value)
        {

            decimal val = 1;
            if (!decimal.TryParse(value, out val))//if a decimal then return it
            {

                if (!_calcInfo.Variables.ContainsKey(value))
                {
                    string err = string.Format("Invalid Variable: '{0}'", value);
                    if (value == ",")
                        err += ";\nDid you mean to call an InLine Function? If so, you must have the Function name against the following '('.";
                    ThrowEquationException(err);
                }
                if (!_istestMode)
                    val = _calcInfo.EvalVariable(value, this.InputObj);
                else
                    val = 1;
            }
            return val;

        }
        #endregion


        private class ExprCalculationInfo
        {

            #region properties
            //new Dictionary<string, decimal>();
            // new Dictionary<string, Func<object,decimal>>();
            //internal delegate decimal IL_Funcs(decimal[] vals);
            internal struct InLineFuncContainer
            {
                internal InLineFuncContainer(Func<decimal[], decimal> fn, int argNumber, string desc)
                {
                    InLineFunction = fn;
                    ArgumentNumber = argNumber;
                    Description = desc;
                }
                
                public Func<decimal[], decimal> InLineFunction;
                public int ArgumentNumber;
                public string Description;
            }


            private Dictionary<string, decimal> _constants = new Dictionary<string, decimal>();
            internal Dictionary<string, decimal> Constants
            {
                get
                {                    
                    return _constants;
                }
            }


            private Dictionary<string, Func<dynamic, decimal>> _variables = new Dictionary<string, Func<dynamic, decimal>>();
            internal Dictionary<string, Func<dynamic, decimal>> Variables
            {
                get
                {
                    return _variables;
                }
            }

            private static Dictionary<string,string> _inLine_FunctionDescriptions = null;
            internal Dictionary<string, string> InLine_FunctionDescriptions
            {
                get
                {
                    if (_inLine_FunctionDescriptions == null)
                    {
                        _inLine_FunctionDescriptions = new Dictionary<string, string>();
                        foreach (KeyValuePair<string, InLineFuncContainer> fn in InLine_Functions)
                        {
                            //descriptions and arguments 
                            _inLine_FunctionDescriptions.Add(fn.Key, string.Format("{0} : ({1})", fn.Value.Description, fn.Value.ArgumentNumber));
                        }
                    }
                    return _inLine_FunctionDescriptions;
                }
            }

            private static Dictionary<string, InLineFuncContainer> _inLine_Functions = null;
            internal Dictionary<string, InLineFuncContainer> InLine_Functions
            {
                get
                {
                    if (_inLine_Functions == null)//share this as static
                    {
                        _inLine_Functions = new Dictionary<string, InLineFuncContainer>();
                        _inLine_Functions.Add("ABS", new InLineFuncContainer(
                            (decimal[] vals) =>
                            { 
                                if (vals.Length != 1) 
                                    ThrowInvalidInLineArgs();
                                return Math.Abs(vals[0]); },
                            1,
                            "Absoulte Value ABS(arg1)"));

                        _inLine_Functions.Add("FLR", new InLineFuncContainer(
                            (decimal[] vals) =>
                            {
                                if (vals.Length != 1)
                                    ThrowInvalidInLineArgs(); 
                                return Math.Floor(vals[0]); },
                             1,
                            "Floor FLR(arg1)"));

                        _inLine_Functions.Add("CEL", new InLineFuncContainer(
                            (decimal[] vals) =>
                            {
                                if (vals.Length != 1)
                                    ThrowInvalidInLineArgs();
                                return Math.Ceiling(vals[0]); },
                             1,
                            "Ceiling CEL(arg1)"));

                        _inLine_Functions.Add("EXP", new InLineFuncContainer(
                            (decimal[] vals) =>
                            {
                                if (vals.Length != 1)
                                    ThrowInvalidInLineArgs(); 
                                return (decimal)Math.Exp((double)vals[0]); },
                             1,
                            "Exponential EXP(arg1)"));

                        _inLine_Functions.Add("_LN", new InLineFuncContainer(
                            (decimal[] vals) =>
                            { 
                                if (vals.Length != 1) 
                                    ThrowInvalidInLineArgs(); 
                                return (decimal)Math.Log((double)vals[0]); },
                             1,
                            "Natural Log _LN(arg1)"));

                        _inLine_Functions.Add("SQRT", new InLineFuncContainer(
                            (decimal[] vals) =>
                            { 
                                if (vals.Length != 1) 
                                    ThrowInvalidInLineArgs(); 
                                return (decimal)Math.Sqrt((double)vals[0]); },
                             1,
                             "Square Root SQRT(arg1)"));


                        //2 params//

                        _inLine_Functions.Add("RND", new InLineFuncContainer(
                            (decimal[] vals) =>
                            { 
                                if (vals.Length != 2) 
                                    ThrowInvalidInLineArgs(); 
                                return Math.Round(vals[0], (int)vals[1]);},
                             2,
                            "Round RND(arg1,Precision)"));

                        _inLine_Functions.Add("MAX", new InLineFuncContainer(
                            (decimal[] vals) =>
                            { 
                                if (vals.Length != 2) 
                                    ThrowInvalidInLineArgs(); 
                                return Math.Max(vals[0], vals[1]);},
                             2,
                            "Max MAX(arg1,arg2)"));

                        _inLine_Functions.Add("MIN", new InLineFuncContainer(
                            (decimal[] vals) =>
                            { 
                                if (vals.Length != 2) 
                                    ThrowInvalidInLineArgs(); 
                                return Math.Min(vals[0], vals[1]);},
                             2,
                            "Min MIN(arg1,arg2)"));
                    }
                    return _inLine_Functions;
                }
            }

            #endregion

            /// <summary>
            /// def constr
            /// </summary>
            internal ExprCalculationInfo()
            {

            }


            #region private
            private static void ThrowInvalidInLineArgs()
            {
                throw new Exception("Invalid Number of Arguments");
            }

            #endregion

            #region internal
            internal decimal EvalVariable(string vName, dynamic inputObj)
            {
                if (Variables.ContainsKey(vName))
                    return Variables[vName].Invoke(inputObj);
                else
                    throw new Exception(string.Format("Invalid Variable: {0}", vName));
            }


            internal decimal ExecuteInlineFunc(string fName, decimal[] args)
            {
                if (InLine_Functions.ContainsKey(fName))
                {
                    var fnObj = InLine_Functions[fName];
                    if(fnObj.ArgumentNumber != args.Length)
                        throw new Exception(string.Format("Invalid Number of parameters for: {0}; requires {1}, supplied {2}", fName, fnObj.ArgumentNumber, args.Length));
                    return fnObj.InLineFunction.Invoke(args);
                }
                else
                    throw new Exception(string.Format("Invalid InLine Function: {0}", fName));
            }

            #endregion
        }
    }
}
